/**
 * 
 */
package org.simplextensions.registry;

import org.simplextensions.Utils;
import org.simplextensions.annotations.ExtensionStyle;
import org.simplextensions.ioc.DependencyInjector;
import org.simplextensions.graph.Graph;
import org.simplextensions.graph.GraphEvent;
import org.simplextensions.graph.GraphEventListenerAdapter;
import org.simplextensions.graph.NodeAlreadyExistsException;
import org.simplextensions.registry.exceptions.ExtensionsConfigurationException;
import org.simplextensions.registry.exceptions.ExtensionsInstantiationsException;
import org.simplextensions.registry.phaselisteners.*;
import org.simplextensions.scanner.impl.ClasspathScanner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.util.*;

/**
 * @author Tomasz Krzyzak, <a
 *         href="mailto:tomasz.krzyzak@gmail.com">tomasz.krzyzak@gmail.com</a>
 * @since 2009-08-04 10:29:32
 * 
 */
public class ExtensionRegistry implements IExtensionRegistry {

	private static final Logger log = LoggerFactory.getLogger(ExtensionRegistry.class);

	Map<String, ExtensionPoint> extensionPoints = new HashMap<String, ExtensionPoint>();
	Map<String, ExtensionPoint> validExtensionPointsMap = new HashMap<String, ExtensionPoint>();

	Set<Extension> extensions = new HashSet<Extension>();

	private ClasspathScanner classPathScanner;

	private Graph structureGraph = new Graph("StructureGraph");
	private Graph validationGraph = new Graph("ValidationGraph");

	private final Collection<SimpleXtensionsBundle> bundles = new LinkedList<SimpleXtensionsBundle>();

	public ExtensionRegistry() {
		classPathScanner = new ClasspathScanner();
		structureGraph.addGraphListener(new GraphEventListenerAdapter() {

			// when node lost some connection it means that node dependencies
			// are not available, therefore is must not be available through
			// IExtensionRegistry interface
			public void nodeLostConnections(GraphEvent graphEvent) {
				validationGraph.removeNode(graphEvent.data);
			}

			// when node is fully connected is means that all dependencies are
			// available;
			public void nodeFullyConnected(GraphEvent graphEvent) {
				ConfigurableElem data = (ConfigurableElem) graphEvent.data;
				if (validateConfigurationElem(data)) {
					String[] connections = structureGraph.getOutgoingNodesIds(data);
					if (data instanceof ExtensionPoint) {
						try {
							validationGraph.addNode(data.getId(), connections, data);
						} catch (NodeAlreadyExistsException e) {
							log.error("", e);
						}
					} else if (data instanceof Extension) {
						Extension extension = (Extension) data;
						try {
							validationGraph.addNode(extension.getPointId() + "::" + data.getId(), connections, data);
						} catch (NodeAlreadyExistsException e) {
							log.error("", e);
						}
					}
				}
			}
		});

		validationGraph.addGraphListener(new GraphEventListenerAdapter() {

			public void nodeLostConnections(GraphEvent graphEvent) {
				if (graphEvent.data instanceof ExtensionPoint) {
					validExtensionPointsMap.remove(graphEvent.id);

				}
			}

			public void nodeFullyConnected(GraphEvent graphEvent) {
				if (graphEvent.data instanceof ExtensionPoint) {
					validExtensionPointsMap.put(graphEvent.id, (ExtensionPoint) graphEvent.data);
				}
			}

		});
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.simplextensions.core.IExtensionRegistry#getExtensionPoint(java.lang
	 * .String)
	 */
	public ExtensionPoint getExtensionPointMD(String extensionPointId) {
		return this.validExtensionPointsMap.get(extensionPointId);
	}

	public void removeExtensionPoint(ExtensionPoint extensionPoint) {
		log.debug("Removing extensionPoint: " + extensionPoint.getId());
		this.extensionPoints.remove(extensionPoint);

		this.structureGraph.removeNode(extensionPoint);
	}

	public void addExtensionPoint(ExtensionPoint extensionPoint) {
		log.debug("Adding extensionPoint: " + extensionPoint.getId());
		this.extensionPoints.put(extensionPoint.getId(), extensionPoint);
		try {
			this.structureGraph.addNode(extensionPoint.getId(), extensionPoint.getDepends(), extensionPoint);
		} catch (NodeAlreadyExistsException e) {
			throw new ExtensionsConfigurationException("", e);
		}

		// if valid add to graph and add validate all invalid extensions
	}

	public void removeExtension(Extension extension) {
		log.debug("Removing extension: " + extension.getId());
		this.extensions.remove(extension);

		this.structureGraph.removeNode(extension);
	}

	public void bindExtension(Extension extension, String pointId) {
		log.debug("Binding extension: " + extension.getId() + " to: " + pointId);
		extension.setDepends(Utils.merge(extension.getDepends(), pointId));
		extension.setPointId(pointId);

		if (extension.getPointId() == null) {
			throw new ExtensionsConfigurationException("binding extension to null extension annotatedPoint");
		}

		try {
			this.structureGraph.addNode(extension.getPointId() + "::" + extension.getId(), extension.getDepends(), extension);
		} catch (NodeAlreadyExistsException e) {
			throw new ExtensionsConfigurationException("", e);
		}
	}

	public void addExtension(Extension extension, String pointId) {
		this.extensions.add(extension);
		if (pointId != null)
			bindExtension(extension, pointId);
	}

	public Object createExecutable(ConfigurableElem configurableElem) {

		if (ExtensionStyle.VIRTUAL == configurableElem.getExtensionStyle()) {
			throw new ExtensionsInstantiationsException("cannot instantiate virtual elem");
		}
		if (ExtensionStyle.SINGLETON == configurableElem.getExtensionStyle() && configurableElem.getFactoryMethodName() == null) {
			throw new ExtensionsConfigurationException("style is singleton but no factory method is provideed");
		}

		log.trace("!!! creating executable: " + configurableElem.getId());

		Object executable;
		if (ExtensionStyle.SINGLETON == configurableElem.getExtensionStyle()) {
			Method method;
			try {
				method = configurableElem.getClazz().getMethod(configurableElem.getFactoryMethodName());
				executable = method.invoke(null);
			} catch (Exception e) {
				throw new ExtensionsInstantiationsException("cannot instantiate elem", e);
			}
		} else {
			try {
				executable = configurableElem.getClazz().newInstance();
			} catch (Exception e) {
				throw new ExtensionsInstantiationsException("cannot instantiate elem", e);
			}
		}

		DependencyInjector.resolveDependency(this, executable);
		return executable;
	}

	public void addBundle(SimpleXtensionsBundle bundle) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
		bundle.addScanListener(org.simplextensions.annotations.ExtensionPoint.class, new ExtensionPointClassScanner(), 0);
		bundle.addScanListener(org.simplextensions.annotations.Extension.class, new ExtensionScanPhaseListener(), 1);
		bundle.addScanListener(org.simplextensions.registry.services.Service.class, new ServiceScanPhaseListener(), 1);
		bundle.addScanListener(org.simplextensions.annotations.DependencyLocator.class, new DependencyLocatorScanPhaseListener(), 1);

		bundle.addScanListener(org.simplextensions.annotations.ExtensionPoint.class, new PropertiesScanPhaseListener(), 2);
		bundle.addScanListener(org.simplextensions.annotations.Extension.class, new PropertiesScanPhaseListener(), 2);
		bundle.addScanListener(org.simplextensions.annotations.Properties.class, new PropertiesScanPhaseListener(), 2);

		classPathScanner.scannClassPath(bundle.getScannerBundle());

		bundles.add(bundle);
	}

	private boolean validateConfigurationElem(ConfigurableElem elem) {
		boolean valid = elem.getId() != null;
		if (elem instanceof Extension) {
			Extension extension = (Extension) elem;
			valid &= extension.getPointId() != null;
		}
		return valid;
	}

	// ----------------------------------- IExtensionRegistry
	// --------------------------

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.simplextensions.core.IExtensionRegistry#getExtensionPoints()
	 */
	public Collection<ExtensionPoint> getExtensionPointsMD() {
		return Collections.unmodifiableCollection(this.validExtensionPointsMap.values());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.simplextensions.core.IExtensionRegistry#getExtensions(java.lang.String
	 * )
	 */
	public Collection<Extension> getExtensionsMD(String extensionPointId) {
		ExtensionPoint extensionPoint = this.validExtensionPointsMap.get(extensionPointId);
		Collection<Extension> result = new LinkedList<Extension>();
		if (extensionPoint != null) {
			for (Extension e : this.extensions)
				if (extensionPointId.equals(e.getPointId()))
					result.add(e);
		}
		return result;
	}

	public Collection<Extension> getExtensionsMDByClass(String name) {
		Collection<Extension> result = new LinkedList<Extension>();
		for (Extension e : this.extensions) {
			if (e.getClassName() != null && e.getClassName().equals(name)) {
				result.add(e);
			}
		}
		return result;
	}

	public Collection<ExtensionPoint> getExtensionPointsMDByClass(String name) {
		Collection<ExtensionPoint> result = new LinkedList<ExtensionPoint>();
		for (ExtensionPoint e : this.validExtensionPointsMap.values()) {
			if (e.getClassName() != null && e.getClassName().equals(name)) {
				result.add(e);
			}
		}
		return result;
	}

	public <T> T getExtensionPoint(Class<T> clazz) {
		return getExtensionPoint(clazz, null);
	}

	public <T> T getExtensionPoint(Class<T> clazz, String id) {
		ExtensionPoint result = null;
		for (ExtensionPoint ep : this.validExtensionPointsMap.values()) {
			if (clazz.isAssignableFrom(ep.getClazz()) && (id == null || (id != null && id.equals(ep.getId())))) {
				result = ep;
				break;
			}
		}
		if (result != null) {
			T executable = (T) result.getExecutable();

			if (!result.isStarted()) {
				Collection<Extension> extensions = getExtensionsMD(result.getId());
				for (Extension e : extensions) {
					result.addExtension(e);
				}
				result.start();
			}
			return executable;
		} else
			return null;
	}


}
